/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.util;

import java.util.*;

/**
 * Generic {@link Comparator} for elements of different types.<br>
 * Consider using {@link ElementValueComparator} if possible.
 * 
 * @param <C> The element type
 * @author M. Hautle
 */
public class CrossElementComparator<C> implements Comparator<C> {
	/** The accessors for the properties. */
	private final List<IValueAccessor[]> properties = new ArrayList<IValueAccessor[]>(2);

	/** The corresponding types for {@link #properties}. */
	private final List<Class<?>> kind = new ArrayList<Class<?>>(2);

	/** Factor describing how the corresponding {@link #properties} entry will be sorted ascending (1) or descending (-1). */
	private final int[] order;

	/**
	 * Creates the accessors for the given properties.
	 * 
	 * @param <T> The type
	 * @param type The element type
	 * @param properties The properties
	 * @return The corresponding accessors
	 */
	@SuppressWarnings("unchecked")
	public static <T> IValueAccessor<T>[] createPropertyAccessors(Class<T> type, String... properties) {
		final IValueAccessor<T>[] acc = new IValueAccessor[properties.length];
		for (int i = 0; i < properties.length; i++)
			acc[i] = new PropertyAccessor<T>(type, properties[i]);
		return acc;
	}

	/**
	 * Creates the accessors for arrays with the given size.
	 * 
	 * @param <T> The type of the array
	 * @param size The numer of elements in the arrays
	 * @return The corresponding accessors
	 */
	@SuppressWarnings("unchecked")
	public static <T> IValueAccessor<T[]>[] createArrayAccessors(int size) {
		final IValueAccessor<T[]>[] acc = new IValueAccessor[size];
		for (int i = 0; i < size; i++)
			acc[i] = new ArrayAccessor<T[]>(i);
		return acc;
	}

	/**
	 * Default constructor.<br>
	 * Adds also an array accessor so that an array containing key elements can be compared with the other elements.
	 * 
	 * @param type The bean type
	 * @param properties The properties building the key (they have to implement {@link Comparable}!)
	 */
	public CrossElementComparator(Class<C> type, String... properties) {
		this(type, createPropertyAccessors(type, properties));
		addAccessors(Object[].class, createArrayAccessors(properties.length));
	}

	/**
	 * Default constructor.
	 * 
	 * @param type The type for which the accessors have to be used
	 * @param accessors The accessors to use
	 */
	public CrossElementComparator(Class type, IValueAccessor... accessors) {
		order = new int[accessors.length];
		Arrays.fill(order, 1);
		addAccessors(type, accessors);
	}

	/**
	 * Adds the accessors for a given type.
	 * 
	 * @param type The type for which the accessors have to be used
	 * @param properties The properties building the key (they have to implement {@link Comparable}!)
	 * @return This comparator
	 * @throws IllegalArgumentException If the number of passed accessors is not equals to them passed in the constructor
	 */
	public CrossElementComparator<C> addAccessors(Class<?> type, String... properties) throws IllegalArgumentException {
		return addAccessors(type, createPropertyAccessors(type, properties));
	}

	/**
	 * Adds the accessors for a given type.
	 * 
	 * @param type The type for which the accessors have to be used
	 * @param accessors The accessors to use
	 * @return This comparator
	 * @throws IllegalArgumentException If the number of passed accessors is not equals to them passed in the constructor
	 */
	public CrossElementComparator<C> addAccessors(Class type, IValueAccessor... accessors) throws IllegalArgumentException {
		if (accessors.length != order.length)
			throw new IllegalArgumentException("You must add " + order.length + " accessors! (The same number as passed initially in the constructor)");
		properties.add(accessors);
		kind.add(type);
		return this;
	}

	/**
	 * Sorts the corresponding property (order from the constructor) ascending/descending.<br>
	 * The value of the last parameter will be used for all other properties in the comparator.
	 * 
	 * @param ascending True to sort ascending false to sort descending - the position describes for which property the setting will be used
	 * @return This comparator
	 */
	public CrossElementComparator<C> setOrder(boolean... ascending) {
		for (int i = 0; i < ascending.length; i++)
			order[i] = ascending[i] ? 1 : -1;
		if (ascending.length == order.length)
			return this;
		final int asc = ascending[ascending.length - 1] ? 1 : -1;
		Arrays.fill(order, ascending.length, order.length, asc);
		return this;
	}

	/**
	 * Sets the sort order of the specified property.
	 * 
	 * @param index The index of the property (order from constructor)
	 * @param ascending True to sort ascending false to sort descending
	 * @return This comparator
	 */
	public CrossElementComparator<C> setOrder(int index, boolean ascending) {
		order[index] = ascending ? 1 : -1;
		return this;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int compare(C o1, C o2) {
		if (o1 == o2)
			return 0;
		if (o1 == null)
			return -1;
		if (o2 == null)
			return -1;
		final IValueAccessor<Comparable>[] one = getAccessors(o1);
		final IValueAccessor<Comparable>[] two = getAccessors(o2);
		for (int i = 0; i < one.length; i++) {
			final Comparable v1 = one[i].getValue(o1, null);
			final Comparable v2 = two[i].getValue(o2, null);
			final int res = compareProperty(v1, v2);
			if (res != 0)
				return res * order[i];
		}
		return 0;
	}

	/**
	 * Returns the accessors to use for the passed argument.
	 * 
	 * @param <T> The type of the accessor
	 * @param arg The argument
	 * @return The corresponding accessors
	 */
	@SuppressWarnings("unchecked")
	private <T> IValueAccessor<T>[] getAccessors(C arg) {
		final Class<?> type = arg.getClass();
		for (int i = 0, cnt = kind.size(); i < cnt; i++) {
			if (kind.get(i).isAssignableFrom(type))
				return properties.get(i);
		}
		throw new NoAccessorFoundException(type);
	}

	/**
	 * Compares two property values.
	 * 
	 * @param o1 The first value or null
	 * @param o2 The second value or null
	 * @return The comparation result
	 */
	@SuppressWarnings("unchecked")
	private int compareProperty(Comparable o1, Comparable o2) {
		if (o1 == o2)
			return 0;
		if (o1 == null)
			return -1;
		if (o2 == null)
			return -1;
		return o1.compareTo(o2);
	}

	/**
	 * Accessors for arrays.
	 * 
	 * @param <T> The type to access
	 * @author M. Hautle
	 */
	private static class ArrayAccessor<T> implements IValueAccessor<T> {
		/** The index to access. */
		private final int index;

		/**
		 * Default constructor.
		 * 
		 * @param index The index to access
		 */
		public ArrayAccessor(int index) {
			this.index = index;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public String getPath() {
			return "[" + index + "]";
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Class<T> getType() {
			return null;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		@SuppressWarnings("unchecked")
		public T getValue(Object element, T fallback) {
			final Object[] vals = (Object[]) element;
			return vals != null && index < vals.length ? (T) vals[index] : fallback;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public boolean setValue(Object element, T value) {
			return false;
		}
	}

	/**
	 * Exception thrown if no accessors were found.<br>
	 * Accessors have to be defined using {@link CrossElementComparator#addAccessors(Class, IValueAccessor...)}.
	 * 
	 * @author M. Hautle
	 */
	public static class NoAccessorFoundException extends RuntimeException {
		/**
		 * Default constructor.
		 * 
		 * @param type The type for which no accessors were found
		 */
		public NoAccessorFoundException(Class type) {
			super("No accessors defined for " + type.getName());
		}
	}
}
